﻿package com.zouloux.core.navigation
{
	import com.asual.swfaddress.SWFAddress;
	import com.asual.swfaddress.SWFAddressEvent;
	import com.zouloux.core.debug.Debug;
	import com.zouloux.core.events.NavigationErrorEvent;
	import com.zouloux.core.events.NavigationEvent;
	import com.zouloux.core.IDisposable;
	import com.zouloux.core.masters.IModel;
	import com.zouloux.core.masters.IView;
	import com.zouloux.core.masters.MasterController;
	import com.zouloux.core.masters.MasterModel;
	import com.zouloux.core.masters.MasterView;
	import flash.display.Loader;
	import flash.display.Sprite;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.IOErrorEvent;
	import flash.net.URLLoader;
	import flash.net.URLRequest;
	import flash.system.ApplicationDomain;
	import flash.utils.getDefinitionByName;
	
	// Les events
	[Event(name="structureLoadError",  type="com.zouloux.core.events.NavigationErrorEvent")]
	[Event(name="structureParseError",  type="com.zouloux.core.events.NavigationErrorEvent")]
	[Event(name="notFound",  type="com.zouloux.core.events.NavigationErrorEvent")]
	[Event(name="pageIn",  type="com.zouloux.core.events.NavigationEvent")]
	[Event(name="pageOut",  type="com.zouloux.core.events.NavigationEvent")]
	[Event(name="pageChange",  type="com.zouloux.core.events.NavigationEvent")]
	
	/**
	 * ...
	 * @author ZoulouX
	 */
	public class Bootstrap extends EventDispatcher implements IDisposable
	{
		/**
		 * L'instance du bootstrap
		 */
		private static var __instance				:Bootstrap;
		
		/**
		 * Récupérer l'instance
		 */
		public static function get instance ():Bootstrap
		{
			// Vérifier si une instance existe
			if (__instance == null)
				return new Bootstrap();
			else
				return __instance;
		}
		
		/**
		 * Configurer le bootstrap automatiquement
		 * @param	pStructure : Le lien vers le fichier XML de la structure du site ou le XML lui même
		 * @param	pApplicationPath : Le lien vers le code source des vues, exemple: "com.yourname.yourapp.view"
		 */
		public static function auto (pStructure:*, pApplicationPath:String = ""):void
		{
			Debug.core(Bootstrap, "auto");
			
			// Charger la structure
			if (pStructure is String)
				Bootstrap.instance.loadStructure(pStructure);
			else if (pStructure is XML)
				Bootstrap.instance.structure = pStructure;
			else
				throw new Error("Bootstrap's structure must be an XML link or an XML structure");
			
			// Démarrer
			instance.start();
			
			// Se souvenir du chemin de l'application
			instance.applicationPath = pApplicationPath;
		}
		
		/**
		 * Le loader de la structure
		 */
		protected var _structureLoader				:URLLoader;
		
		/**
		 * La structure
		 */
		protected var _structure					:XML;
		
		/**
		 * Le chemin vers les classes de l'application
		 */
		protected var _applicationPath				:String;
		
		/**
		 * Le domaine d'application
		 */
		protected var _domain						:ApplicationDomain;
		
		/**
		 * Les différentes routes
		 */
		protected var _routes						:Array;
		
		/**
		 * Les différents menus
		 */
		protected var _menus						:Array;
		
		/**
		 * Le container
		 */
		protected var _viewContainer				:Sprite							= new Sprite();
		
		/**
		 * La vue courante
		 */
		protected var _currentView					:MasterView;
		
		/**
		 * Le nom de la vue courante
		 */
		protected var _currentViewName				:String;
		
		/**
		 * Vérifier si la structure est prête
		 */
		protected var _structureReady				:Boolean 						= false;
		
		/**
		 * Vérifier si on doit initialiser une fois que la structure est prête
		 */
		protected var _waitForInit					:Boolean						= false;
		
		/**
		 * La vue en cours de transition
		 */
		protected var _transitionView				:MasterView;
		
		/**
		 * Défini si les routes doivent avoir la même action pour correspondre avec le menu
		 */
		protected var _strictMenuRoute				:Boolean						= false;
		
		
		/**
		 * Récupérer la structure XML
		 */
		public function get structure ():XML { return _structure; }
		public function set structure (value:XML):void
		{
			// Enregistrer la structure
			_structure = value;
			
			// Essayer de charger la structure
			try 
			{
				// Parser la structure
				parseStructure();
				
				// Parser les menus
				parseMenus();
				
				// C'est prêt
				_structureReady = true;
				
				// On dispatche les évènement
				dispatchEvent(new NavigationEvent(NavigationEvent.STRUCTURE_READY));
			}
			catch (error:Error)
			{
				// Dispatcher l'erreur de parsing
				dispatchEvent(new NavigationErrorEvent(NavigationErrorEvent.STRUCTURE_PARSE_ERROR, error.message));
			}
			
			// Vérifier si on doit balancer l'init
			if (_structureReady && _waitForInit)
				changeHandler();
		}
		
		/**
		 * L'url actuelle de l'application
		 */
		public function get url ():String { return SWFAddress.getValue(); }
		public function set url (value:String):void
		{
			SWFAddress.setValue(value);
		}
		
		/**
		 * Le lien vers l'application pour charger les vues
		 */
		public function get applicationPath ():String { return _applicationPath; }
		public function set applicationPath(value:String):void 
		{
			_applicationPath = value;
		}
		
		/**
		 * Le domaine d'application (domaine courrant de l'application)
		 */
		public function get domain():ApplicationDomain { return _domain; }
		public function set domain(value:ApplicationDomain):void 
		{
			_domain = value;
		}
		
		/**
		 * Le container pour les vues
		 */
		public function get viewContainer():Sprite { return _viewContainer; }
		public function set viewContainer(value:Sprite):void 
		{
			_viewContainer = value;
		}
		
		/**
		 * Défini si les routes doivent avoir la même action pour correspondre avec le menu
		 */
		public function get strictMenuRoute():Boolean { return _strictMenuRoute; }
		public function set strictMenuRoute(value:Boolean):void 
		{
			_strictMenuRoute = value;
		}
		
		/**
		 * Récupérer l'instance de la vue courante
		 */
		public function get currentView():MasterView { return _currentView; }
		
		
		/**
		 * Le constructeur
		 */
		public function Bootstrap ()
		{
			// Vérifier s'il y a déjà une instance
			if (__instance == null)
				__instance = this;
			else throw new Error("Bootstrap is Singleton. Only one instance is alowed!");
			
			// Récupérer le domaine
			_domain = ApplicationDomain.currentDomain;
		}
		
		/**
		 * Charger la structeur au format XML
		 * @param	pUrl
		 */
		public function loadStructure (pUrl:String):void
		{
			// Créer le loader
			_structureLoader = new URLLoader(new URLRequest(pUrl));
			
			// Tendre l'oreille pour écouter ce qu'il se passe t'entend?
			_structureLoader.addEventListener(Event.COMPLETE, loadStructureCompleteHandler);
			_structureLoader.addEventListener(IOErrorEvent.IO_ERROR, loadStructureErrorHandler);
		}
		
		/**
		 * Le chargement de la structure XML est terminé
		 * @param	event
		 */
		protected function loadStructureCompleteHandler (event:Event):void 
		{
			// Essayer de charger la structure
			try
			{
				// Convertir
				structure = new XML(_structureLoader.data as String);
			}
			catch (error:Error)
			{
				// Dispatcher l'erreur de parsing
				dispatchEvent(new NavigationErrorEvent(NavigationErrorEvent.STRUCTURE_PARSE_ERROR, error.message));
			}
			
			// Killer le loader
			disposeLoader();
		}
		
		/**
		 * Il y a eu une erreur lors du chargement des données XML
		 * @param	event
		 */
		protected function loadStructureErrorHandler (event:IOErrorEvent):void 
		{
			Debug.core(this, "loadStructureErrorHandler");
			
			// Dispatcher l'erreur
			dispatchEvent(new NavigationErrorEvent(NavigationErrorEvent.STRUCTURE_LOAD_ERROR));
			
			// Killer le loader
			disposeLoader();
		}
		
		/**
		 * Disposer le loader
		 */
		protected function disposeLoader ():void
		{
			// Ne plus ecouter
			_structureLoader.removeEventListener(Event.COMPLETE, loadStructureCompleteHandler);
			_structureLoader.removeEventListener(IOErrorEvent.IO_ERROR, loadStructureErrorHandler);
			_structureLoader = null;
		}
		
		/**
		 * Parser la structure
		 */
		protected function parseStructure ():void
		{
			// Créer la structure
			_routes = [];
			
			// Parser les pages
			for each (var page:XML in _structure..page as XMLList)
			{
				_routes.push(new RouteItem(this, page));
			}
		}
		
		/**
		 * Parser les menus
		 */
		protected function parseMenus():void
		{
			// Créer la liste des menus
			_menus = [];
			
			// Parser les menus de l'XML
			for each (var menu:XML in _structure..menu as XMLList)
			{
				// L'id, inrémenter par défaut
				var id:String = (menu.@id == "" ? _menus.length.toString() : menu.@id);
				
				// Créer un model abstrait
				_menus[id] = new MasterModel();
				
				// Forcer le dispatch de chaque event pour les menus
				(_menus[id] as MasterModel).collection.forceDispatch = true;
				
				// Parser
				for each (var item:XML in menu.item as XMLList)
				{
					// Essayer de caster en MenuItem			
					(_menus[id] as MasterModel).collection.add(new MenuItem(this, item));
				}
			}
		}
		
		/**
		 * Changement de l'url
		 * @param	event
		 */
		protected function changeHandler (event:SWFAddressEvent = null):void 
		{
			// Vérifier que la structure soit prête
			if (_structureReady)
			{
				// La route que l'on doit trouver
				var selectedRoute:RouteItem;
				
				// Chercher parmis toutes les routes en mémoire
				for each (var route:RouteItem in _routes)
				{
					// Vérifier la correspondance avec l'url
					if (route.checkRoute(SWFAddress.getValue()))
					{
						// On a trouvé, on enregistre, on passe à la suite
						selectedRoute = route;
						break;
					}
				}
				
				// Vérifier si on a trouvé la route
				if (selectedRoute != null)
				{
					// Chercher le menuItem
					for each (var menuModel:IModel in _menus)
					{
						// Vérifier si on doit valider ce model
						var thisModel:Boolean = false;
						
						// Rechercher dans la collection
						for each (var item:MenuItem in menuModel.collection.all)
						{
							// Vérifier si on trouve une route identique
							if (item.view == selectedRoute.view && (!_strictMenuRoute || item.action == selectedRoute.action))
							{
								// On séléctionne l'index de cet item
								menuModel.collection.index = menuModel.collection.getIndex(item);
								
								// On doit valider ce model
								thisModel = true;
								
								// On arrête de chercher pour ne pas en séléctionner plusieurs
								break;
							}
						}
						
						// Valider ce model si on a trouvé son élément
						menuModel.valid = thisModel;
					}
					
					// Appeler la vue
					callView(selectedRoute.view, selectedRoute.action, selectedRoute.vars);
				}
				else
				{
					// 404
					dispatchEvent(new NavigationErrorEvent(NavigationErrorEvent.NOT_FOUND));
				}
				
				// Signaler que la page à changer
				dispatchEvent(new NavigationEvent(NavigationEvent.PAGE_CHANGE));	
			}
			else
			{
				// On attend que ça soit prêt pour initialiser l'application
				_waitForInit = true;
			}
		}
		
		/**
		 * Démarrer le bootstrap automatique
		 */
		public function start ():void
		{
			SWFAddress.addEventListener(SWFAddressEvent.CHANGE, changeHandler);
		}
		
		/**
		 * Arrêter le bootstrap automatique
		 */
		public function stop ():void
		{
			SWFAddress.removeEventListener(SWFAddressEvent.CHANGE, changeHandler);
		}
		
		/**
		 * Appeler et instancier une vue
		 */
		public function callView (pViewName:String, pAction:String = "index", pVars:Object = null):void
		{
			Debug.core(this, "callView", arguments);
			
			// Initialiser le tableau null
			if (pVars == null)
				pVars = {};
			
			// Si on est pas déjà sur la bonne vue
			if (_currentViewName != pViewName)
			{
				// Essayer
				try
				{
					// D'instancier
					var tempView:MasterView = instanciateView(pViewName);
					
					// Appeler l'action de cette vue
					callAction(tempView, pAction, pVars);
					
					// Vérifier si une vue existe actuellement
					if (_currentView != null)
					{
						// oldView c'est la futur ancienne vue
						var oldView:MasterView = _currentView;
						
						// Vérifier s'il y a une vue en cours de transition
						if (_transitionView != null)
						{
							// Dispatcher un faux event pour debugguer
							_transitionView.dispatchEvent(new NavigationEvent(NavigationEvent.PAGE_OUT));
						}
						
						// Cette vue est en cours de transition
						_transitionView = oldView;
						
						// Remplacer
						_currentView = tempView;
						_currentViewName = pViewName;
						
						// Ecouter lorsque l'animation de sortie est terminée
						oldView.addEventListener(NavigationEvent.PAGE_OUT, showCurrentView);
						
						// Essayer
						try
						{
							// Faire l'animation de sortie
							oldView.playOut();
						}
						catch (error:Error)
						{
							// Dispatcher un faux event pour debugguer
							oldView.dispatchEvent(new NavigationEvent(NavigationEvent.PAGE_OUT));
						}
						
						// Dispatcher
						dispatchEvent(new NavigationEvent(NavigationEvent.PAGE_OUT));
					}
					else
					{
						// Séléctionne comme étant la vue courante
						_currentView = tempView
						_currentViewName = pViewName;
						
						// Ajouter à la displayList
						_viewContainer.addChild(_currentView);
						
						// Jouer la vue
						try
						{
							_currentView.playIn();
						}
						catch (error:Error)
						{
							throw new Error("Unable to playIn the view");
						}
						
						// Dispatcher le pageIn
						dispatchEvent(new NavigationEvent(NavigationEvent.PAGE_IN));
					}
				}
				catch (error:Error)
				{
					// Oops!
					dispatchEvent(new NavigationErrorEvent(NavigationErrorEvent.NOT_FOUND, error.message));
				}
			}
			else
			{
				// Essayer
				try
				{
					// Juste appeler l'action de cette vue
					callAction(_currentView, pAction, pVars);
				}
				catch (error:Error)
				{
					// Oops!
					dispatchEvent(new NavigationErrorEvent(NavigationErrorEvent.NOT_FOUND, error.message));
				}
			}
		}
		
		/**
		 * Afficher la vue courante une fois que la vue précédente à fini son playOut
		 * @param	event
		 */
		protected function showCurrentView (event:NavigationEvent):void
		{
			Debug.core(this, "showCurrentView", arguments);
			
			// Il n'y a plus de vue en transition
			_transitionView = null;
			
			// Cibler l'ancienne vue
			var oldView:MasterView = (event.currentTarget as MasterView);
			
			// Ne plus ecouter
			oldView.removeEventListener(NavigationEvent.PAGE_OUT, showCurrentView);
			
			// Effacer l'ancienne vue de container
			if (_viewContainer.contains(oldView))
				_viewContainer.removeChild(oldView);
			
			// Ajouter à la displayList
			_viewContainer.addChild(_currentView);
			
			// Faire le playIn sur la nouvelle vue
			try
			{
				_currentView.playIn();
			}
			catch (error:Error)
			{
				throw new Error("Unable to playIn the view");
			}
			
			// Dispatcher le pageIn
			dispatchEvent(new NavigationEvent(NavigationEvent.PAGE_IN));
		}
		
		/**
		 * Instancier une vue, retourner l'instance de cette vue
		 * @param	pViewName : 
		 */
		protected function instanciateView (pViewName:String):MasterView
		{
			// Récupérer le chemin de la vue
			var viewPath:String = _applicationPath + "." + pViewName;
			
			// Vérifier que cette classe existe
			if (_domain.hasDefinition(viewPath))
			{
				// Récupérer la classe du controlleur
				var ViewClass:Class = Class(_domain.getDefinition(viewPath));
				
				// La varible vers l'instance du controller
				var viewInstance:MasterView;
				
				// Instancier ce controlleur
				try
				{
					// Tenter d'instancier
					viewInstance = new ViewClass() as MasterView;
					
					// Retourner le controlleur
					return viewInstance;
				}
				catch (e:Error)
				{
					throw new Error("Bootstrap Error. A critical error occured while view instanciation. The view '" + pViewName + "' must extends MasterView. (" + e.message + ") " + e.getStackTrace());
				}
			}
			else throw new Error("Bootstrap Error. View '" + pViewName + "' not found.");
			return new MasterView();
		}
		
		/**
		 * Appeler une action
		 */
		protected function callAction (pView:MasterView, pActionName:String = "index", pVars:Object = null):void
		{
			// Initialiser le tableau null
			if (pVars == null)
				pVars = {};
			
			// Vérifier si le controlleur existe
			if (pView.controller != null)
			{
				// Vérifier que l'action existe et que c'est bien une action
				if (pView.controller[pActionName] != null && pView.controller[pActionName] is Function)
				{
					// Passer les paramètres
					pView.controller.params = pVars;
					
					// Appeler
					pView.controller[pActionName].apply(pView.controller);
				}
				else throw new Error("Bootstrap Error. Unable to call action "+pActionName+" in controller.");
			}
			else throw new Error("Bootstrap Error. Controller of not found.");
		}
		
		/**
		 * Récupérer un menu par son id
		 * @param	pMenuId
		 */
		public function getMenu (pMenuId:String = ""):IModel
		{
			if (_menus[pMenuId] != null)
			{
				return _menus[pMenuId];
			}
			else throw new Error("Menu not found in structure");
		}
		
		/**
		 * Changer l'url de navigation
		 */
		public function changeURL (pURL:String):void
		{
			// Enregistrer simplement l'url
			url = pURL;
		}
		
		/**
		 * Créer une URL
		 * @param	pView : Le nom de la vue
		 * @param	pAction : Le nom de l'action (index par défaut)
		 * @param	pVars : Les variables
		 * @return
		 */
		public function makeURL (pView:String, pAction:String = "index", pVars:Object = null):String
		{
			// Initialiser le tableau null
			if (pVars == null)
				pVars = {};
			
			// On parcour toutes les routes
			for each (var route:RouteItem in _routes)
			{
				// On vérifie si on en trouve une de valide
				// Si oui on retourne le reverse
				if (route.checkReverse(pView, pAction, pVars))
					return route.reverseURL(pVars);
			}
			
			throw new Error("No route found");
			return "";
		}
		
		/**
		 * Ouvrir une page
		 * @param	pView : Le nom de la vue
		 * @param	pAction : Le nom de l'action (index par défaut)
		 * @param	pVars : Les variables
		 * @return
		 */
		public function open (pView:String, pAction:String = "index", pVars:Object = null):void
		{
			Debug.core(this, "open", arguments);
			
			// Initialiser le tableau null
			if (pVars == null)
				pVars = {};
			
			// Essayer
			try
			{
				// De changer l'url
				var url:String = makeURL(pView, pAction, pVars);
				
				// Si l'url est trouvée
				if (url != "")
					changeURL(url);
			}
			catch (error:Error)
			{
				// Error not found
				dispatchEvent(new NavigationErrorEvent(NavigationErrorEvent.NOT_FOUND, error.message));
				trace(error.getStackTrace());
			}
		}
		
		/**
		 * Effacer le bootstrap
		 */
		public function dispose ():void
		{
			stop();
			disposeLoader();
			if (_currentView != null)
				_currentView.removeEventListener(NavigationEvent.PAGE_OUT, showCurrentView);
		}
	}
}